Skip to main content

建立你自己的完全链上 DAO 来投资 NFT

为您的 NFT 持有者构建 DAO

什么是 DAO?

DAO 代表去中心化自治组织。您可以将 DAO 视为类似于现实世界中的公司。从本质上讲,DAO 允许成员创建治理决策并对其进行投票。

在传统公司中,当需要做出决定时,公司的董事会或高管负责做出该决定。然而,在 DAO 中,这个过程是民主化的,任何成员都可以创建提案,所有其他成员都可以对其进行投票。创建的每个提案都有一个投票截止日期,在截止日期之后做出有利于投票结果的决定(是或否)。

DAO 的成员资格通常受到 ERC20 代币所有权或 NFT 所有权的限制。成员资格和投票权与您拥有的代币数量成正比的 DAO 示例包括 UniswapENS。基于 NFT 的 DAO 示例包括 Meebits DAO

构建我们的 DAO

你想为你的 CryptoDevs NFT 的持有者启动一个 DAO。从通过 ICO 获得的 ETH 中,你建立了一个 DAO 库。 DAO 现在有很多 ETH,但目前什么也没做。

您希望允许您的 NFT 持有者创建并投票使用该 ETH 从 NFT 市场购买其他 NFT 的提案,并推测价格。也许将来当你卖回 NFT 时,你会将利润分配给 DAO 的所有成员。

要求

  • 任何拥有 CryptoDevs NFT 的人都可以创建从 NFT 市场购买不同 NFT 的提案
  • 每个拥有 CryptoDevs NFT 的人都可以投票支持或反对活跃的提案
  • 每个 NFT 计为每个提案的一票
  • 投票者不能对具有相同 NFT 的同一个提案多次投票
  • 如果在截止日期前多数选民投票支持该提案,NFT 购买将自动执行

我们将做什么

  • 为了能够在提案通过时自动购买 NFT,您需要一个可以调用 purchase() 函数的链上 NFT 市场。那里有很多 NFT 市场,但为了避免过于复杂,我们将为本教程创建一个简化的假 NFT 市场,因为重点是 DAO。
  • 我们还将使用 Hardhat 制作实际的 DAO 智能合约。
  • 我们将使用 Next.js 制作网站,以允许用户创建和对提案进行投票

先决条件

  • 你已经完成了之前的 NFT Collection 教程。
  • 你必须有一些 ETH 给 DAO 财政部

构建

智能合约开发

我们将从创建智能合约开始。我们将制作两个智能合约:

  • FakeNFTMarketplace.sol
  • CryptoDevsDAO.sol

为此,我们将使用我们在过去几个教程中一直使用的 Hardhat 开发框架。

为这个项目创建一个名为 DAO-Tutorial 的文件夹,并在该文件夹中打开一个终端窗口。

通过在终端中运行以下命令来设置新的安全帽项目:

mkdir hardhat-tutorial
cd hardhat-tutorial
npm init --yes
npm install --save-dev hardhat

现在您已经安装了 Hardhat,我们可以设置一个项目。在终端中执行以下命令。

在安装 Hardhat 的同一目录中运行:

npx hardhat

确保选择创建 Javascript 项目,然后按照终端中的步骤完成安全帽设置。

现在,让我们从 NPM 安装 @openzeppelin/contracts 包,因为我们将使用 OpenZeppelin 的 Ownable Contract 作为 DAO 合约。

npm install @openzeppelin/contracts

首先,让我们做一个简单的 Fake NFT Marketplace。在 hardhat-tutorial 中的 contracts 目录下创建一个名为 FakeNFTMarketplace.sol 的文件,并添加以下代码。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FakeNFTMarketplace {
/// @dev Maintain a mapping of Fake TokenID to Owner addresses
mapping(uint256 => address) public tokens;
/// @dev Set the purchase price for each Fake NFT
uint256 nftPrice = 0.1 ether;

/// @dev purchase() accepts ETH and marks the owner of the given tokenId as the caller address
/// @param _tokenId - the fake NFT token Id to purchase
function purchase(uint256 _tokenId) external payable {
require(msg.value == nftPrice, "This NFT costs 0.1 ether");
tokens[_tokenId] = msg.sender;
}

/// @dev getPrice() returns the price of one NFT
function getPrice() external view returns (uint256) {
return nftPrice;
}

/// @dev available() checks whether the given tokenId has already been sold or not
/// @param _tokenId - the tokenId to check for
function available(uint256 _tokenId) external view returns (bool) {
// address(0) = 0x0000000000000000000000000000000000000000
// This is the default value for addresses in Solidity
if (tokens[_tokenId] == address(0)) {
return true;
}
return false;
}
}

FakeNFTMarketplace 公开了一些基本功能,如果提案获得通过,我们将从 DAO 合约中使用这些功能来购买 NFT。 真正的 NFT 市场会更加复杂——因为并非所有 NFT 的价格都相同。

在开始编写 DAO 合约之前,让我们确保一切都编译好。 在终端的 hardhat-tutorial 文件夹中运行以下命令。

npx hardhat compile

并确保没有编译错误。

现在,我们将开始编写 CryptoDevsDAO 合约。 由于这主要是一个完全自定义的合约,并且比我们目前所做的相对复杂,所以我们将一点一点地解释这一点。 首先,让我们为合约编写样板代码。 在 hardhat-tutorial 的 contracts 目录下创建一个名为 CryptoDevsDAO.sol 的新文件,并将以下代码添加到其中。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";

// We will add the Interfaces here

contract CryptoDevsDAO is Ownable {
// We will write contract code here
}

现在,我们需要调用 FakeNFTMarketplace 合约以及您之前部署的 CryptoDevs NFT 合约的函数。 回想一下高级 Solidity 主题教程,我们需要为这些合约提供一个接口,因此该合约知道哪些函数可以调用,它们将什么作为参数以及它们返回什么。

通过添加以下代码将以下两个接口添加到您的代码中

/**
* Interface for the FakeNFTMarketplace
*/
interface IFakeNFTMarketplace {
/// @dev getPrice() returns the price of an NFT from the FakeNFTMarketplace
/// @return Returns the price in Wei for an NFT
function getPrice() external view returns (uint256);

/// @dev available() returns whether or not the given _tokenId has already been purchased
/// @return Returns a boolean value - true if available, false if not
function available(uint256 _tokenId) external view returns (bool);

/// @dev purchase() purchases an NFT from the FakeNFTMarketplace
/// @param _tokenId - the fake NFT tokenID to purchase
function purchase(uint256 _tokenId) external payable;
}

/**
* Minimal interface for CryptoDevsNFT containing only two functions
* that we are interested in
*/
interface ICryptoDevsNFT {
/// @dev balanceOf returns the number of NFTs owned by the given address
/// @param owner - address to fetch number of NFTs for
/// @return Returns the number of NFTs owned
function balanceOf(address owner) external view returns (uint256);

/// @dev tokenOfOwnerByIndex returns a tokenID at given index for owner
/// @param owner - address to fetch the NFT TokenID for
/// @param index - index of NFT in owned tokens array to fetch
/// @return Returns the TokenID of the NFT
function tokenOfOwnerByIndex(address owner, uint256 index)
external
view
returns (uint256);
}

现在,让我们考虑一下我们在 DAO 合约中需要哪些功能。

  • 以合约状态存储已创建的提案
  • 允许 CryptoDevs NFT 的持有者创建新提案
  • 允许 CryptoDevs NFT 的持有者对提案进行投票,因为他们尚未投票,并且提案尚未通过其截止日期
  • 允许 CryptoDevs NFT 的持有者在超过截止日期后执行提案,以在提案通过时触发 NFT 购买

让我们从创建一个表示提案的结构开始。 在您的合约中,添加以下代码:

// Create a struct named Proposal containing all relevant information
struct Proposal {
// nftTokenId - the tokenID of the NFT to purchase from FakeNFTMarketplace if the proposal passes
uint256 nftTokenId;
// deadline - the UNIX timestamp until which this proposal is active. Proposal can be executed after the deadline has been exceeded.
uint256 deadline;
// yayVotes - number of yay votes for this proposal
uint256 yayVotes;
// nayVotes - number of nay votes for this proposal
uint256 nayVotes;
// executed - whether or not this proposal has been executed yet. Cannot be executed before the deadline has been exceeded.
bool executed;
// voters - a mapping of CryptoDevsNFT tokenIDs to booleans indicating whether that NFT has already been used to cast a vote or not
mapping(uint256 => bool) voters;
}

让我们还创建一个从提案 ID 到提案的映射以保存所有创建的提案,以及一个计数器来计算存在的提案数量。

// Create a mapping of ID to Proposal
mapping(uint256 => Proposal) public proposals;
// Number of proposals that have been created
uint256 public numProposals;

现在,由于我们将调用 FakeNFTMarketplace 和 CryptoDevsNFT 合约上的函数,让我们为这些合约初始化变量。

IFakeNFTMarketplace nftMarketplace;
ICryptoDevsNFT cryptoDevsNFT;

创建一个构造函数来初始化这些合约变量,并接受部署者的 ETH 存款以填充 DAO ETH 库。 (在后台,由于我们导入了 Ownable 合约,这也会将合约部署者设置为该合约的所有者)

// Create a payable constructor which initializes the contract
// instances for FakeNFTMarketplace and CryptoDevsNFT
// The payable allows this constructor to accept an ETH deposit when it is being deployed
constructor(address _nftMarketplace, address _cryptoDevsNFT) payable {
nftMarketplace = IFakeNFTMarketplace(_nftMarketplace);
cryptoDevsNFT = ICryptoDevsNFT(_cryptoDevsNFT);
}

现在,由于我们希望几乎所有其他函数只能由拥有 CryptoDevs NFT 合约的 NFT 的人调用,因此我们将创建一个修饰符以避免重复代码。

// Create a modifier which only allows a function to be
// called by someone who owns at least 1 CryptoDevsNFT
modifier nftHolderOnly() {
require(cryptoDevsNFT.balanceOf(msg.sender) > 0, "NOT_A_DAO_MEMBER");
_;
}

现在,要对提案进行投票,我们要添加一个额外的限制,即被投票的提案不得超过其截止日期。 为此,我们将创建第二个修饰符。

注意这个修饰符是如何接受参数的!

此外,由于投票只能是两个值之一(是或否) - 我们可以创建一个表示可能选项的枚举。

编写 voteOnProposal 函数

/// @dev voteOnProposal allows a CryptoDevsNFT holder to cast their vote on an active proposal
/// @param proposalIndex - the index of the proposal to vote on in the proposals array
/// @param vote - the type of vote they want to cast
function voteOnProposal(uint256 proposalIndex, Vote vote)
external
nftHolderOnly
activeProposalOnly(proposalIndex)
{
Proposal storage proposal = proposals[proposalIndex];

uint256 voterNFTBalance = cryptoDevsNFT.balanceOf(msg.sender);
uint256 numVotes = 0;

// Calculate how many NFTs are owned by the voter
// that haven't already been used for voting on this proposal
for (uint256 i = 0; i < voterNFTBalance; i++) {
uint256 tokenId = cryptoDevsNFT.tokenOfOwnerByIndex(msg.sender, i);
if (proposal.voters[tokenId] == false) {
numVotes++;
proposal.voters[tokenId] = true;
}
}
require(numVotes > 0, "ALREADY_VOTED");

if (vote == Vote.YAY) {
proposal.yayVotes += numVotes;
} else {
proposal.nayVotes += numVotes;
}
}

我们快完成了! 要执行已超过截止日期的提案,我们将创建最终修改器。

// Create a modifier which only allows a function to be
// called if the given proposals' deadline HAS been exceeded
// and if the proposal has not yet been executed
modifier inactiveProposalOnly(uint256 proposalIndex) {
require(
proposals[proposalIndex].deadline <= block.timestamp,
"DEADLINE_NOT_EXCEEDED"
);
require(
proposals[proposalIndex].executed == false,
"PROPOSAL_ALREADY_EXECUTED"
);
_;
}

注意这个修饰符也需要一个参数!

让我们编写 executeProposal 的代码

/// @dev executeProposal allows any CryptoDevsNFT holder to execute a proposal after it's deadline has been exceeded
/// @param proposalIndex - the index of the proposal to execute in the proposals array
function executeProposal(uint256 proposalIndex)
external
nftHolderOnly
inactiveProposalOnly(proposalIndex)
{
Proposal storage proposal = proposals[proposalIndex];

// If the proposal has more YAY votes than NAY votes
// purchase the NFT from the FakeNFTMarketplace
if (proposal.yayVotes > proposal.nayVotes) {
uint256 nftPrice = nftMarketplace.getPrice();
require(address(this).balance >= nftPrice, "NOT_ENOUGH_FUNDS");
nftMarketplace.purchase{value: nftPrice}(proposal.nftTokenId);
}
proposal.executed = true;
}

至此,我们已经实现了所有核心功能。 但是,我们可以并且应该实现一些附加功能。

  • 如果需要,允许合约所有者从 DAO 中提取 ETH
  • 允许合约接受更多的 ETH 存款

我们继承的 Ownable 合约包含一个修饰符 onlyOwner,它将函数限制为只能由合约所有者调用。 让我们使用该修饰符来实现 withdrawEther。

/// @dev withdrawEther allows the contract owner (deployer) to withdraw the ETH from the contract
function withdrawEther() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}

这会将合约的全部 ETH 余额转移到所有者地址

最后,为了允许向 DAO 库中添加更多 ETH 存款,需要添加一些特殊功能。 通常,合约地址不能接受发送给他们的 ETH,除非是通过支付功能。 但是我们不希望用户调用函数只是为了存钱,他们应该能够直接从他们的钱包中转移 ETH。 为此,让我们添加这两个函数:

// The following two functions allow the contract to accept ETH deposits
// directly from a wallet without calling a function
receive() external payable {}

fallback() external payable {}

智能合约部署

创建 .env 配置,并安装 dotenv 依赖

QUICKNODE_HTTP_URL 从 Quicknode 中获取。PRIVATE_KEY 从钱包中获取。

QUICKNODE_HTTP_URL="add-quicknode-http-provider-url-here"
PRIVATE_KEY="add-the-private-key-here"

添加部署代码

scripts/deploy.js

const { ethers } = require("hardhat");
const { CRYPTODEVS_NFT_CONTRACT_ADDRESS } = require("../constants");

async function main() {
// Deploy the FakeNFTMarketplace contract first
const FakeNFTMarketplace = await ethers.getContractFactory(
"FakeNFTMarketplace"
);
const fakeNftMarketplace = await FakeNFTMarketplace.deploy();
await fakeNftMarketplace.deployed();

console.log("FakeNFTMarketplace deployed to: ", fakeNftMarketplace.address);

// Now deploy the CryptoDevsDAO contract
const CryptoDevsDAO = await ethers.getContractFactory("CryptoDevsDAO");
const cryptoDevsDAO = await CryptoDevsDAO.deploy(
fakeNftMarketplace.address,
CRYPTODEVS_NFT_CONTRACT_ADDRESS,
{
// This assumes your account has at least 1 ETH in it's account
// Change this value as you want
value: ethers.utils.parseEther("1"),
}
);
await cryptoDevsDAO.deployed();

console.log("CryptoDevsDAO deployed to: ", cryptoDevsDAO.address);
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

添加部署代码需要的变量

constants/index.js

// Replace the value with your NFT contract address
const CRYPTODEVS_NFT_CONTRACT_ADDRESS =
"YOUR_CRYPTODEVS_NFT_CONTRACT_ADDRESS_HERE";

module.exports = { CRYPTODEVS_NFT_CONTRACT_ADDRESS };

修改 hardhat.config.js 配置

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config({ path: ".env" });

const QUICKNODE_HTTP_URL = process.env.QUICKNODE_HTTP_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;

module.exports = {
solidity: "0.8.4",
networks: {
goerli: {
url: QUICKNODE_HTTP_URL,
accounts: [PRIVATE_KEY],
},
},
};

编译

npx hardhat compile

部署

npx hardhat run scripts/deploy.js --network goerli

保存合约地址

前端开发

创建 my-app 项目

npx create-next-app@latest

安装依赖

npm install web3modal ethers

修改样式

my-app/styles/Home.modules.css

.main {
min-height: 90vh;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-family: "Courier New", Courier, monospace;
}

.footer {
display: flex;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}

.image {
width: 70%;
height: 50%;
margin-left: 20%;
}

.title {
font-size: 2rem;
margin: 2rem 0;
}

.description {
line-height: 1;
margin: 2rem 0;
font-size: 1.2rem;
}

.button {
border-radius: 4px;
background-color: blue;
border: none;
color: #ffffff;
font-size: 15px;
padding: 10px;
width: 200px;
cursor: pointer;
margin-right: 2%;
}

.button2 {
border-radius: 4px;
background-color: indigo;
border: none;
color: #ffffff;
font-size: 15px;
padding: 10px;
cursor: pointer;
margin-right: 2%;
margin-top: 1rem;
}

.proposalCard {
width: fit-content;
margin-top: 0.25rem;
border: black 2px solid;
flex: 1;
flex-direction: column;
}

.container {
margin-top: 2rem;
}

.flex {
flex: 1;
justify-content: space-between;
}

@media (max-width: 1000px) {
.main {
width: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
}

配置合约地址和接口

该网站还需要从两个智能合约——CryptoDevsDAO 和 CryptoDevsNFT 读取/写入数据。 让我们将他们的合约地址和 ABI 存储在一个常量文件中。 在 my-app 目录中创建一个 constants.js 文件。

constants/index.js

export const CRYPTODEVS_DAO_CONTRACT_ADDRESS = "";
export const CRYPTODEVS_NFT_CONTRACT_ADDRESS = "";

export const CRYPTODEVS_DAO_ABI = [];
export const CRYPTODEVS_NFT_ABI = [];

修改页面

pages/index.js

import { Contract, providers } from "ethers";
import { formatEther } from "ethers/lib/utils";
import Head from "next/head";
import { useEffect, useRef, useState } from "react";
import Web3Modal from "web3modal";
import {
CRYPTODEVS_DAO_ABI,
CRYPTODEVS_DAO_CONTRACT_ADDRESS,
CRYPTODEVS_NFT_ABI,
CRYPTODEVS_NFT_CONTRACT_ADDRESS,
} from "../constants";
import styles from "../styles/Home.module.css";

export default function Home() {
// ETH Balance of the DAO contract
const [treasuryBalance, setTreasuryBalance] = useState("0");
// Number of proposals created in the DAO
const [numProposals, setNumProposals] = useState("0");
// Array of all proposals created in the DAO
const [proposals, setProposals] = useState([]);
// User's balance of CryptoDevs NFTs
const [nftBalance, setNftBalance] = useState(0);
// Fake NFT Token ID to purchase. Used when creating a proposal.
const [fakeNftTokenId, setFakeNftTokenId] = useState("");
// One of "Create Proposal" or "View Proposals"
const [selectedTab, setSelectedTab] = useState("");
// True if waiting for a transaction to be mined, false otherwise.
const [loading, setLoading] = useState(false);
// True if user has connected their wallet, false otherwise
const [walletConnected, setWalletConnected] = useState(false);
const web3ModalRef = useRef();

// Helper function to connect wallet
const connectWallet = async () => {
try {
await getProviderOrSigner();
setWalletConnected(true);
} catch (error) {
console.error(error);
}
};

// Reads the ETH balance of the DAO contract and sets the `treasuryBalance` state variable
const getDAOTreasuryBalance = async () => {
try {
const provider = await getProviderOrSigner();
const balance = await provider.getBalance(
CRYPTODEVS_DAO_CONTRACT_ADDRESS
);
setTreasuryBalance(balance.toString());
} catch (error) {
console.error(error);
}
};

// Reads the number of proposals in the DAO contract and sets the `numProposals` state variable
const getNumProposalsInDAO = async () => {
try {
const provider = await getProviderOrSigner();
const contract = getDaoContractInstance(provider);
const daoNumProposals = await contract.numProposals();
setNumProposals(daoNumProposals.toString());
} catch (error) {
console.error(error);
}
};

// Reads the balance of the user's CryptoDevs NFTs and sets the `nftBalance` state variable
const getUserNFTBalance = async () => {
try {
const signer = await getProviderOrSigner(true);
const nftContract = getCryptodevsNFTContractInstance(signer);
const balance = await nftContract.balanceOf(signer.getAddress());
setNftBalance(parseInt(balance.toString()));
} catch (error) {
console.error(error);
}
};

// Calls the `createProposal` function in the contract, using the tokenId from `fakeNftTokenId`
const createProposal = async () => {
try {
const signer = await getProviderOrSigner(true);
const daoContract = getDaoContractInstance(signer);
const txn = await daoContract.createProposal(fakeNftTokenId);
setLoading(true);
await txn.wait();
await getNumProposalsInDAO();
setLoading(false);
} catch (error) {
console.error(error);
window.alert(error.data.message);
}
};

// Helper function to fetch and parse one proposal from the DAO contract
// Given the Proposal ID
// and converts the returned data into a Javascript object with values we can use
const fetchProposalById = async (id) => {
try {
const provider = await getProviderOrSigner();
const daoContract = getDaoContractInstance(provider);
const proposal = await daoContract.proposals(id);
const parsedProposal = {
proposalId: id,
nftTokenId: proposal.nftTokenId.toString(),
deadline: new Date(parseInt(proposal.deadline.toString()) * 1000),
yayVotes: proposal.yayVotes.toString(),
nayVotes: proposal.nayVotes.toString(),
executed: proposal.executed,
};
return parsedProposal;
} catch (error) {
console.error(error);
}
};

// Runs a loop `numProposals` times to fetch all proposals in the DAO
// and sets the `proposals` state variable
const fetchAllProposals = async () => {
try {
const proposals = [];
for (let i = 0; i < numProposals; i++) {
const proposal = await fetchProposalById(i);
proposals.push(proposal);
}
setProposals(proposals);
return proposals;
} catch (error) {
console.error(error);
}
};

// Calls the `voteOnProposal` function in the contract, using the passed
// proposal ID and Vote
const voteOnProposal = async (proposalId, _vote) => {
try {
const signer = await getProviderOrSigner(true);
const daoContract = getDaoContractInstance(signer);

let vote = _vote === "YAY" ? 0 : 1;
const txn = await daoContract.voteOnProposal(proposalId, vote);
setLoading(true);
await txn.wait();
setLoading(false);
await fetchAllProposals();
} catch (error) {
console.error(error);
window.alert(error.data.message);
}
};

// Calls the `executeProposal` function in the contract, using
// the passed proposal ID
const executeProposal = async (proposalId) => {
try {
const signer = await getProviderOrSigner(true);
const daoContract = getDaoContractInstance(signer);
const txn = await daoContract.executeProposal(proposalId);
setLoading(true);
await txn.wait();
setLoading(false);
await fetchAllProposals();
} catch (error) {
console.error(error);
window.alert(error.data.message);
}
};

// Helper function to fetch a Provider/Signer instance from Metamask
const getProviderOrSigner = async (needSigner = false) => {
const provider = await web3ModalRef.current.connect();
const web3Provider = new providers.Web3Provider(provider);

const { chainId } = await web3Provider.getNetwork();
if (chainId !== 5) {
window.alert("Please switch to the Goerli network!");
throw new Error("Please switch to the Goerli network");
}

if (needSigner) {
const signer = web3Provider.getSigner();
return signer;
}
return web3Provider;
};

// Helper function to return a DAO Contract instance
// given a Provider/Signer
const getDaoContractInstance = (providerOrSigner) => {
return new Contract(
CRYPTODEVS_DAO_CONTRACT_ADDRESS,
CRYPTODEVS_DAO_ABI,
providerOrSigner
);
};

// Helper function to return a CryptoDevs NFT Contract instance
// given a Provider/Signer
const getCryptodevsNFTContractInstance = (providerOrSigner) => {
return new Contract(
CRYPTODEVS_NFT_CONTRACT_ADDRESS,
CRYPTODEVS_NFT_ABI,
providerOrSigner
);
};

// piece of code that runs everytime the value of `walletConnected` changes
// so when a wallet connects or disconnects
// Prompts user to connect wallet if not connected
// and then calls helper functions to fetch the
// DAO Treasury Balance, User NFT Balance, and Number of Proposals in the DAO
useEffect(() => {
if (!walletConnected) {
web3ModalRef.current = new Web3Modal({
network: "goerli",
providerOptions: {},
disableInjectedProvider: false,
});

connectWallet().then(() => {
getDAOTreasuryBalance();
getUserNFTBalance();
getNumProposalsInDAO();
});
}
}, [walletConnected]);

// Piece of code that runs everytime the value of `selectedTab` changes
// Used to re-fetch all proposals in the DAO when user switches
// to the 'View Proposals' tab
useEffect(() => {
if (selectedTab === "View Proposals") {
fetchAllProposals();
}
}, [selectedTab]);

// Render the contents of the appropriate tab based on `selectedTab`
function renderTabs() {
if (selectedTab === "Create Proposal") {
return renderCreateProposalTab();
} else if (selectedTab === "View Proposals") {
return renderViewProposalsTab();
}
return null;
}

// Renders the 'Create Proposal' tab content
function renderCreateProposalTab() {
if (loading) {
return (
<div className={styles.description}>
Loading... Waiting for transaction...
</div>
);
} else if (nftBalance === 0) {
return (
<div className={styles.description}>
You do not own any CryptoDevs NFTs. <br />
<b>You cannot create or vote on proposals</b>
</div>
);
} else {
return (
<div className={styles.container}>
<label>Fake NFT Token ID to Purchase: </label>
<input
placeholder="0"
type="number"
onChange={(e) => setFakeNftTokenId(e.target.value)}
/>
<button className={styles.button2} onClick={createProposal}>
Create
</button>
</div>
);
}
}

// Renders the 'View Proposals' tab content
function renderViewProposalsTab() {
if (loading) {
return (
<div className={styles.description}>
Loading... Waiting for transaction...
</div>
);
} else if (proposals.length === 0) {
return (
<div className={styles.description}>No proposals have been created</div>
);
} else {
return (
<div>
{proposals.map((p, index) => (
<div key={index} className={styles.proposalCard}>
<p>Proposal ID: {p.proposalId}</p>
<p>Fake NFT to Purchase: {p.nftTokenId}</p>
<p>Deadline: {p.deadline.toLocaleString()}</p>
<p>Yay Votes: {p.yayVotes}</p>
<p>Nay Votes: {p.nayVotes}</p>
<p>Executed?: {p.executed.toString()}</p>
{p.deadline.getTime() > Date.now() && !p.executed ? (
<div className={styles.flex}>
<button
className={styles.button2}
onClick={() => voteOnProposal(p.proposalId, "YAY")}
>
Vote YAY
</button>
<button
className={styles.button2}
onClick={() => voteOnProposal(p.proposalId, "NAY")}
>
Vote NAY
</button>
</div>
) : p.deadline.getTime() < Date.now() && !p.executed ? (
<div className={styles.flex}>
<button
className={styles.button2}
onClick={() => executeProposal(p.proposalId)}
>
Execute Proposal{" "}
{p.yayVotes > p.nayVotes ? "(YAY)" : "(NAY)"}
</button>
</div>
) : (
<div className={styles.description}>Proposal Executed</div>
)}
</div>
))}
</div>
);
}
}

return (
<div>
<Head>
<title>CryptoDevs DAO</title>
<meta name="description" content="CryptoDevs DAO" />
<link rel="icon" href="/favicon.ico" />
</Head>

<div className={styles.main}>
<div>
<h1 className={styles.title}>Welcome to Crypto Devs!</h1>
<div className={styles.description}>Welcome to the DAO!</div>
<div className={styles.description}>
Your CryptoDevs NFT Balance: {nftBalance}
<br />
Treasury Balance: {formatEther(treasuryBalance)} ETH
<br />
Total Number of Proposals: {numProposals}
</div>
<div className={styles.flex}>
<button
className={styles.button}
onClick={() => setSelectedTab("Create Proposal")}
>
Create Proposal
</button>
<button
className={styles.button}
onClick={() => setSelectedTab("View Proposals")}
>
View Proposals
</button>
</div>
{renderTabs()}
</div>
<div>
<img className={styles.image} src="/cryptodevs/0.svg" />
</div>
</div>

<footer className={styles.footer}>
Made with &#10084; by Crypto Devs
</footer>
</div>
);
}

运行

pnpm run dev

测试

  • 创建几个提案
  • 尝试在一个上投 YAY,在另一个上投 NAY
  • 等待 5 分钟让他们的最后期限过去
  • 执行这两个。
  • 由于在执行时购买 NFT 的提案获得通过,DAO 财政部的余额减少了 0.1 ETH。

推送到 github

部署